﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LockCrypt.Core {
    /// <summary>
    /// Analyzes the entropy of passwords.
    /// </summary>
    public class PasswordAnalyzer {
        /// <summary>
        /// Contains bonuses and penalties relating to a password.
        /// </summary>
        public class PasswordAnalysis {
            public decimal PercentageScore { get; set; }
            public int BitStrength { get; set; }

            #region Bonuses
            public int Length { get; set; }
            public int HasUppercase { get; set; }
            public int HasLowercase { get; set; }
            public int HasNumbers { get; set; }
            public int HasSymbols { get; set; }
            public int HasNumbersSymbols { get; set; }
            #endregion

            #region Penalties
            public int OnlyLetters { get; set; }
            public int OnlyNumbers { get; set; }
            public int RepeatedCharacters { get; set; }
            public int SubsequentsUppercaseLetters { get; set; }
            public int SubsequentLowercaseLetters { get; set; }
            public int SubsequentNumbers { get; set; }
            public int SequentialLetters { get; set; }
            public int SequentialNumbers { get; set; }
            #endregion
        }

        /// <summary>
        /// Analyzes a password.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns>An analysis of the password</returns>
        public static PasswordAnalysis CheckStrength(string password) {
            int nScore = 0,

                // Character type counts
                nAlphaUC = 0,
                nAlphaLC = 0,
                nNumber = 0,
                nSymbol = 0,
                nMidChar = 0,
                nRepChar = 0,
                nConsecAlphaUC = 0,
                nConsecAlphaLC = 0,
                nConsecNumber = 0,
                nConsecSymbol = 0,
                nConsecCharType = 0,
                nSeqAlpha = 0,
                nSeqNumber = 0,
                nSeqChar = 0,

                // Buffers to check for repeated subsequent characters
                nTmpAlphaUC = 0,
                nTmpAlphaLC = 0,
                nTmpNumber = 0,
                nTmpSymbol = 0;

            const int nMultLength = 4,
                      nMultNumber = 4,
                      nMultSymbol = 6,
                      nMultMidChar = 2,

                      // Penalty multipliers
                      nMultConsecAlphaUC = 2,
                      nMultConsecAlphaLC = 2,
                      nMultConsecNumber = 2,
                      nMultSeqAlpha = 3,
                      nMultSeqNumber = 3;
            const string sAlphas = "abcdefghijklmnopqrstuvwxyz",
                         sNumerics = "01234567890";

            // http://www.passwordmeter.com/
            PasswordAnalysis analysis = new PasswordAnalysis() {
                Length = password.Length,
                BitStrength = CalculateEntropy(password)
            };
            if(!string.IsNullOrEmpty(password)) {
                nScore = password.Length * nMultLength;
                /* Loop through password to check for Symbol, Numeric, Lowercase and Uppercase pattern matches */
                for(int a = 0; a < password.Length; a++) {
                    char currentChar = password[a];
                    if(char.IsUpper(currentChar)) {
                        if(nTmpAlphaUC > 0 && (nTmpAlphaUC + 1) == a) {
                            nConsecAlphaUC++;
                            nConsecCharType++;
                        }
                        nTmpAlphaUC = a;
                        nAlphaUC++;
                    } else if(char.IsLower(currentChar)) {
                        if(nTmpAlphaLC > 0 && (nTmpAlphaLC + 1) == a) {
                            nConsecAlphaLC++;
                            nConsecCharType++;
                        }
                        nTmpAlphaLC = a;
                        nAlphaLC++;
                    } else if(char.IsDigit(currentChar)) {
                        if(a > 0 && a < (password.Length - 1)) {
                            nMidChar++;
                        }
                        if(nTmpNumber > 0 && (nTmpNumber + 1) == a) {
                            nConsecNumber++;
                            nConsecCharType++;
                        }
                        nTmpNumber = a;
                        nNumber++;
                    } else if(!char.IsLetterOrDigit(currentChar)) {
                        if(a > 0 && a < (password.Length - 1)) {
                            nMidChar++;
                        }
                        if(nTmpSymbol > 0 && (nTmpSymbol + 1) == a) {
                            nConsecSymbol++;
                            nConsecCharType++;
                        }
                        nTmpSymbol = a;
                        nSymbol++;
                    }
                    /* Check for repeated characters */
                    char currentCharLower = char.ToLowerInvariant(currentChar);
                    for(int b = 0; b < password.Length; b++) {
                        if(currentCharLower == char.ToLowerInvariant(password[b]) && a != b) {
                            nRepChar++;
                        }
                    }
                }

                /* Check for sequential alpha string patterns (forward and reverse) */
                for(int s = 0; s < 23; s++) {
                    string sFwd = sAlphas.Substring(s, 3);
                    string sRev = Utils.ReverseString(sFwd);
                    if(password.ToLowerInvariant().Contains(sFwd) || password.ToLowerInvariant().Contains(sRev)) {
                        nSeqAlpha++;
                        nSeqChar++;
                    }
                }

                /* Check for sequential numeric string patterns (forward and reverse) */
                for(int s = 0; s < 8; s++) {
                    String sFwd = sNumerics.Substring(s, 3);
                    string sRev = Utils.ReverseString(sFwd);
                    if(password.ToLowerInvariant().Contains(sFwd) || password.ToLowerInvariant().Contains(sRev)) {
                        nSeqNumber++;
                        nSeqChar++;
                    }
                }

                /* Assign bonuses */
                if(nAlphaUC > 0 && nAlphaUC < password.Length) {
                    nScore = nScore + ((password.Length - nAlphaUC) * 2);
                }
                if(nAlphaLC > 0 && nAlphaLC < password.Length) {
                    nScore = nScore + ((password.Length - nAlphaLC) * 2);
                }
                if(nNumber > 0 && nNumber < password.Length) {
                    nScore = nScore + (nNumber*nMultNumber);
                }
                if(nSymbol > 0) {
                    nScore = nScore + (nSymbol*nMultSymbol);
                }
                if(nMidChar > 0) {
                    nScore = nScore + (nMidChar*nMultMidChar);
                }

                /* Penalties */
                if((nAlphaLC > 0 || nAlphaUC > 0) && nSymbol == 0 && nNumber == 0) {
                    // Only Letters
                    nScore = nScore - (password.Length / 2);
                }
                if(nAlphaLC == 0 && nAlphaUC == 0 && nSymbol == 0 && nNumber > 0) {
                    // Only Numbers
                    nScore = nScore - (password.Length / 2);
                }
                if(nRepChar > 0) {
                    // Same character exists more than once
                    nScore = nScore - ((nRepChar*nRepChar)/2);
                }
                if(nConsecAlphaUC > 0) {
                    // Consecutive Uppercase Letters exist
                    nScore = nScore - (nConsecAlphaUC*nMultConsecAlphaUC);
                }
                if(nConsecAlphaLC > 0) {
                    // Consecutive Lowercase Letters exist
                    nScore = nScore - (nConsecAlphaLC*nMultConsecAlphaLC);
                }
                if(nConsecNumber > 0) {
                    // Consecutive Numbers exist
                    nScore = nScore - (nConsecNumber*nMultConsecNumber);
                }
                if(nSeqAlpha > 0) {
                    // Sequential alpha strings exist (3 characters or more)
                    nScore = nScore - (nSeqAlpha*nMultSeqAlpha);
                }
                if(nSeqNumber > 0) {
                    // Sequential numeric strings exist (3 characters or more)
                    nScore = nScore - (nSeqNumber*nMultSeqNumber);
                }

                /* Bonuses */
                if(nAlphaUC > 0 && nAlphaUC < password.Length) {
                    analysis.HasUppercase = ((password.Length - nAlphaUC) * 2);
                }
                if(nAlphaLC > 0 && nAlphaLC < password.Length) {
                    analysis.HasLowercase = ((password.Length - nAlphaLC) * 2);
                }
                if(nNumber > 0 && nNumber < password.Length) {
                    analysis.HasNumbers = (nNumber*nMultNumber);
                }
                if(nSymbol > 0) {
                    analysis.HasSymbols = (nSymbol*nMultSymbol);
                }
                if(nMidChar > 0) {
                    analysis.HasNumbersSymbols = (nMidChar*nMultMidChar);
                }

                /* Point deductions for poor practices */
                if((nAlphaLC > 0 || nAlphaUC > 0) && nSymbol == 0 && nNumber == 0) {
                    // Only Letters
                    analysis.OnlyLetters = -password.Length;
                }
                if(nAlphaLC == 0 && nAlphaUC == 0 && nSymbol == 0 && nNumber > 0) {
                    // Only Numbers
                    analysis.OnlyNumbers = -password.Length;
                }
                if(nRepChar > 0) {
                    // Same character exists more than once
                    analysis.RepeatedCharacters = -nRepChar;
                }
                if(nConsecAlphaUC > 0) {
                    // Consecutive Uppercase Letters exist
                    analysis.SubsequentLowercaseLetters = -(nConsecAlphaUC*nMultConsecAlphaUC);
                }
                if(nConsecAlphaLC > 0) {
                    // Consecutive Lowercase Letters exist
                    analysis.SubsequentsUppercaseLetters = -(nConsecAlphaLC*nMultConsecAlphaLC);
                }
                if(nConsecNumber > 0) {
                    // Consecutive Numbers exist
                    analysis.SubsequentNumbers = -(nConsecNumber*nMultConsecNumber);
                }
                if(nSeqAlpha > 0) {
                    // Sequential alpha strings exist (3 characters or more)
                    analysis.SequentialLetters = -(nSeqAlpha*nMultSeqAlpha);
                }
                if(nSeqNumber > 0) {
                    // Sequential numeric strings exist (3 characters or more)
                    analysis.SequentialNumbers = -(nSeqNumber*nMultSeqNumber);
                }
            }
            analysis.PercentageScore = nScore > 100 ? 100 : nScore < 0 ? 0 : nScore;
            return analysis;
        }

        /// <summary>
        /// Calculates the maximum entropy a given password may have, given approximate details about it's contents.
        /// </summary>
        /// <param name="specification">The specification.</param>
        /// <returns>The maximum entropy in bits of a password matching the specification.</returns>
        public static int CalculateMaxEntropy(PasswordCreationArgs specification) {
            List<char> pool = new List<char>();
            if(specification.UpperCase) {
                pool.AddRange(PasswordGenerator.UpperCaseChars);
            }
            if(specification.LowerCase) {
                pool.AddRange(PasswordGenerator.LowerCaseChars);
            }
            if(specification.Digits) {
                pool.AddRange(PasswordGenerator.DigitChars);
            }
            if(specification.SpecialCharacters) {
                pool.AddRange(PasswordGenerator.SpecialChars);
            }
            if(specification.Brackets) {
                pool.AddRange(PasswordGenerator.BracketChars);
            }
            if(specification.ExtendedAscii) {
                pool.AddRange(PasswordGenerator.ExtendedChars);
            }
            if(specification.ExtraCharacters != null) {
                pool.AddRange(specification.ExtraCharacters);
            }
            int poolsize = pool.Distinct().Count();
            double bitStrength = Math.Log(Math.Pow(poolsize, specification.Length)) / Math.Log(2);
            return Convert.ToInt32(Math.Round(bitStrength));
        }

        /// <summary>
        /// Calculates the entropy of a password in bits.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns>The approximate bit strength, given the contents of the password.</returns>
        public static int CalculateEntropy(string password) {
            if(string.IsNullOrEmpty(password)) {
                return 0;
            }
            bool hasUpper = false,
                 hasLower = false,
                 hasNumeric = false,
                 hasSymbols = false;
            List<char> pool = new List<char>();
            for(int i = 0; i < password.Length; i++) {
                char currentChar = password[i];
                if(char.IsUpper(currentChar)) {
                    hasUpper = true;
                } else if(char.IsLower(currentChar)) {
                    hasLower = true;
                } else if(char.IsNumber(currentChar)) {
                    hasNumeric = true;
                } else {
                    hasSymbols = true;
                    pool.Add(currentChar);
                }
            }
            if(hasUpper) {
                pool.AddRange(PasswordGenerator.UpperCaseChars);
            }
            if(hasLower) {
                pool.AddRange(PasswordGenerator.LowerCaseChars);
            }
            if(hasNumeric) {
                pool.AddRange(PasswordGenerator.DigitChars);
            }
            int poolSize = pool.Distinct().Count();
            if(hasSymbols) {
                // includes extended and special characters, ie: too many symbols to add, so add each one to the pool (in the for loop above)
                poolSize += 10;
            }

            double bitStrength = Math.Log(Math.Pow(poolSize, password.Length))/Math.Log(2);
            return Convert.ToInt32(Math.Round(bitStrength));
        }
    }
}
